/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Xceed.Silverlight.Data
{
  internal static class QueryableExtensions
  {
    #region REFLECTION METHODS

    internal static MemberInfo GetMemberInfo( Type type, string memberName )
    {
      MemberInfo[] members = type.FindMembers( MemberTypes.Property | MemberTypes.Field,
        BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance,
        Type.FilterNameIgnoreCase, memberName );

      if( members.Length != 0 )
        return members[ 0 ];

      return null;
    }

    #endregion REFLECTION METHODS

    #region ORDERING

    internal static IQueryable OrderBy(
      this IQueryable queryable,
      IEnumerable<SortDescription> implicitSortDescriptions,
      IEnumerable<SortDescription> explicitSortDescriptions,
      bool reverse )
    {
      IEnumerable<string> propertyNames = new string[ 0 ];
      IEnumerable<ListSortDirection> sortDirections = new ListSortDirection[ 0 ];

      if( explicitSortDescriptions != null )
      {
        propertyNames = propertyNames.Concat( from e in explicitSortDescriptions
                                              select e.PropertyName );
        sortDirections = sortDirections.Concat( from e in explicitSortDescriptions
                                                select e.Direction );
      }
      if( implicitSortDescriptions != null )
      {
        propertyNames = propertyNames.Concat( from e in implicitSortDescriptions
                                              select e.PropertyName );
        sortDirections = sortDirections.Concat( from e in implicitSortDescriptions
                                                select e.Direction );
      }

      return QueryableExtensions.OrderBy( queryable,
        propertyNames.ToArray(),
        sortDirections.ToArray(),
        reverse );
    }

    private static IQueryable OrderBy(
      this IQueryable queryable,
      string[] propertyNames,
      ListSortDirection[] sortDirections,
      bool reverse )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( propertyNames == null ) || ( sortDirections == null ) )
        return queryable;

      if( propertyNames.Length != sortDirections.Length )
        throw new InvalidOperationException( "The propertyNames array must have the same length as the sortDirectionArray." );

      Expression orderedExpression = queryable.Expression;

      Type queryableElementType = queryable.ElementType;

      ParameterExpression[] parameters = new ParameterExpression[] { queryable.CreateParameterExpression() };

      string ascendingOrderByMethodName = "OrderBy";
      string descendingOrderByMethodName = "OrderByDescending";

      for( int i = 0; i < propertyNames.Length; i++ )
      {
        MemberExpression sortMemberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], propertyNames[ i ] );

        string methodNameToUse;

        if( ( sortDirections[ i ] == ListSortDirection.Ascending && !reverse ) || ( sortDirections[ i ] == ListSortDirection.Descending && reverse ) )
        {
          methodNameToUse = ascendingOrderByMethodName;
        }
        else
        {
          methodNameToUse = descendingOrderByMethodName;
        }

        orderedExpression = Expression.Call( typeof( Queryable ),
          methodNameToUse,
          new Type[] { queryableElementType, sortMemberExpression.Type },
          orderedExpression, Expression.Quote( Expression.Lambda( sortMemberExpression, parameters ) ) );


        ascendingOrderByMethodName = "ThenBy";
        descendingOrderByMethodName = "ThenByDescending";
      }
      return queryable.Provider.CreateQuery( orderedExpression );
    }

    #endregion ORDERING

    #region FILTERING

    internal static Expression CreateStartsWithExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.StartsWith );
    }

    internal static Expression CreateEndsWithExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.EndsWith );
    }

    internal static Expression CreateContainsExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.Contains );
    }


    internal static Expression CreateGreaterThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.GreaterThan );
    }

    internal static Expression CreateGreaterThanOrEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.GreaterThanOrEqual );
    }


    internal static Expression CreateLesserThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.LessThan );
    }

    internal static Expression CreateLesserThanOrEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.LessThanOrEqual );
    }

    internal static Expression CreateEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.Equal );
    }

    internal static Expression CreateDifferentThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.NotEqual );
    }


    internal static ParameterExpression CreateParameterExpression( this IQueryable queryable )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      return Expression.Parameter( queryable.ElementType, queryable.ElementType.Name.ToLower() );
    }

    internal static Expression CreateBinaryComparison(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      object[] values,
      BinaryComparisonDelegate binaryComparisonDelegate )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( values == null ) || ( values.Length == 0 ) )
        throw new ArgumentException( "values cannot be null or empty.", "values" );

      if( binaryComparisonDelegate == null )
        throw new ArgumentNullException( "binaryComparisonDelegate" );

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( sharedParameterExpression, propertyName );

      Expression mergedFilterExpression = null;

      for( int i = 0; i < values.Length; i++ )
      {
        ConstantExpression valueExpression = Expression.Constant( values[ i ] );

        Expression newFilterExpression = binaryComparisonDelegate( memberExpression, Expression.Convert( valueExpression, memberExpression.Type ) );

        if( mergedFilterExpression == null )
        {
          mergedFilterExpression = newFilterExpression;
        }
        else
        {
          mergedFilterExpression = Expression.Or( mergedFilterExpression, newFilterExpression );
        }
      }

      return mergedFilterExpression;
    }

    internal static IQueryable Where( this IQueryable source, Expression predicate )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Where",
          new Type[] { source.ElementType },
          source.Expression,
          predicate );

      return source.Provider.CreateQuery( methodCall );
    }

    #endregion FILTERING

    #region SELECTING

    internal static IQueryable Select( this IQueryable queryable, string propertyName )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( string.IsNullOrEmpty( propertyName ) )
        throw new ArgumentException( "A property name must be provided.", "propertyName" );

      Type queryableElementType = queryable.ElementType;
      ParameterExpression[] parameters = new ParameterExpression[] { Expression.Parameter( queryableElementType, "" ) };

      MemberExpression selectMemberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], propertyName );

      LambdaExpression selectLambdaExpression = Expression.Lambda( selectMemberExpression, parameters );

      return queryable.Provider.CreateQuery(
        Expression.Call( typeof( Queryable ), "Select",
        new Type[] { queryableElementType, selectLambdaExpression.Body.Type },
        queryable.Expression, Expression.Quote( selectLambdaExpression ) ) );
    }

    internal static IQueryable GetSubGroupsAndCountsQueryable( this IQueryable queryable, string subGroupBy, bool sortGroupBy, ListSortDirection direction )
    {
      // Create GroupBy

      Type queryableElementType = queryable.ElementType;

      ParameterExpression[] parameters = new ParameterExpression[] { Expression.Parameter( queryableElementType, "" ) };

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], subGroupBy );

      LambdaExpression groupByLambdaExpression = Expression.Lambda( memberExpression, parameters );

      MethodCallExpression groupByMethodExpression =
        Expression.Call(
        typeof( Queryable ), "GroupBy",
        new Type[] { queryableElementType, groupByLambdaExpression.Body.Type },
        new Expression[] { queryable.Expression, Expression.Quote( groupByLambdaExpression ) } );

      IQueryable groupedResult = queryable.Provider.CreateQuery( groupByMethodExpression );

      if( sortGroupBy )
        groupedResult = groupedResult.OrderByKey( direction == ListSortDirection.Ascending );

      // Create Select

      ParameterExpression[] groupedParameters = new ParameterExpression[] { Expression.Parameter( groupedResult.ElementType, "" ) };

      UnaryExpression selectKeyExpression = Expression.Convert( Expression.Property( groupedParameters[ 0 ], "Key" ), typeof( object ) );
      UnaryExpression selectCountExpression = Expression.Convert( Expression.Call( typeof( Enumerable ), "Count", new Type[] { queryableElementType }, groupedParameters ), typeof( object ) );


      LambdaExpression finalLambdaExpression = Expression.Lambda(
        Expression.NewArrayInit( typeof( object ), new Expression[] { selectKeyExpression, selectCountExpression } ), groupedParameters );


      MethodCallExpression finalSelectExpression =
        Expression.Call(
        typeof( Queryable ), "Select",
        new Type[] { groupedResult.ElementType, finalLambdaExpression.Body.Type },
        new Expression[] { groupedResult.Expression, Expression.Quote( finalLambdaExpression ) } );


      return groupedResult.Provider.CreateQuery( finalSelectExpression );
    }

    internal static IQueryable Skip( this IQueryable source, int count )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      if( count == 0 )
        return source;

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Skip",
          new Type[] { source.ElementType },
          source.Expression,
          Expression.Constant( count ) );

      return source.Provider.CreateQuery( methodCall );
    }

    internal static IQueryable Take( this IQueryable source, int count )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Take",
          new Type[] { source.ElementType },
          source.Expression,
          Expression.Constant( count ) );

      return source.Provider.CreateQuery( methodCall );
    }

    internal static int Count( this IQueryable queryable )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      Type queryableElementType = queryable.ElementType;
      Expression expressionToCount = queryable.Expression;

      MethodCallExpression countExpression = Expression.Call(
        typeof( Queryable ), "Count",
        new Type[] { queryableElementType }, expressionToCount );

      int count = ( int )queryable.Provider.Execute( countExpression );
      return count;
    }

    #endregion SELECTING

    #region PRIVATE METHODS

    private static IQueryable OrderByKey( this IQueryable queryable, bool ascending )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      Expression orderedExpression = queryable.Expression;

      Type queryableElementType = queryable.ElementType;

      ParameterExpression parameter = queryable.CreateParameterExpression();

      MemberExpression sortMemberExpression = QueryableExtensions.GenerateMemberExpression( parameter, "Key" );

      string methodName = ( ascending ) ? "OrderBy" : "OrderByDescending";

      orderedExpression = Expression.Call( typeof( Queryable ),
            methodName,
            new Type[] { queryableElementType, sortMemberExpression.Type },
            orderedExpression, Expression.Quote( Expression.Lambda( sortMemberExpression, parameter ) ) );

      return queryable.Provider.CreateQuery( orderedExpression );
    }


    internal static MemberExpression GenerateMemberExpression( ParameterExpression typeExpression, string name )
    {
      if( string.IsNullOrEmpty( name ) )
        throw new ArgumentException( "A name must be provided.", "name" );

      MemberExpression memberExpression = null;

      MemberInfo member = QueryableExtensions.GetMemberInfo( typeExpression.Type, name );

      if( member == null )
        throw new InvalidOperationException( "Unable to generate the MemberExpression." );

      if( member is PropertyInfo )
      {
        // Member is a property.
        memberExpression = Expression.Property( typeExpression, ( PropertyInfo )member );
      }
      else
      {
        // Member is a field.
        memberExpression = Expression.Field( typeExpression, ( FieldInfo )member );
      }

      return memberExpression;
    }

    private static string GetMethodNameFromStringFilterMode( StringFilterMode stringFilterMode )
    {
      switch( stringFilterMode )
      {
        case StringFilterMode.StartsWith:
          {
            return "StartsWith";
          }

        case StringFilterMode.Contains:
          {
            return "Contains";
          }

        case StringFilterMode.EndsWith:
          {
            return "EndsWith";
          }
      }

      return string.Empty;
    }

    private static Expression FilterString(
     this IQueryable queryable,
     ParameterExpression sharedParameterExpression,
     string propertyName,
     string[] values,
     StringFilterMode stringFilterMode )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( values == null ) || ( values.Length == 0 ) )
        throw new ArgumentException( "values cannot be null or empty.", "values" );

      string methodName = QueryableExtensions.GetMethodNameFromStringFilterMode( stringFilterMode );

      Debug.Assert( !String.IsNullOrEmpty( methodName ) );

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( sharedParameterExpression, propertyName );

      Expression mergedFilterCall = null;

      for( int i = 0; i < values.Length; i++ )
      {
        ConstantExpression valueExpression = Expression.Constant( values[ i ] );

        MethodCallExpression newFilterCall = Expression.Call(
          memberExpression,
          typeof( string ).GetMethod( methodName, new Type[] { typeof( string ) } ),
          Expression.Convert( valueExpression, memberExpression.Type ) );

        if( mergedFilterCall == null )
        {
          mergedFilterCall = newFilterCall;
        }
        else
        {
          mergedFilterCall = MethodCallExpression.Or( mergedFilterCall, newFilterCall );
        }
      }

      return mergedFilterCall;
    }

    #endregion PRIVATE METHODS

    #region INTERNAL FIELDS

    internal delegate Expression BinaryComparisonDelegate( Expression left, Expression right );




    #endregion INTERNAL FIELDS

    #region PRIVATE NESTED ENUMS

    private enum StringFilterMode
    {
      StartsWith = 0,
      Contains = 1,
      EndsWith = 2
    }

    #endregion PRIVATE NESTED ENUMS
  }
}
